/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.injection;

import java.io.*;
import java.util.*;
import sun.misc.*;
import ch.trackedbean.binding.beanSpec.*;

/**
 * Class loader which implements the calls {@link TrackedBeanAdapter#adapt(byte[])} for each class implementing a specified marker interface.<br>
 * Child classes of the marked classes were also adapted so that they use the feautures of the {@link StatusInformationInternal} interface.
 * 
 * @author M. Hautle
 */
public class TrackedBeanInjection extends ClassLoader implements IClassAnalyzer {
	/** General info entry for java.* classes. */
	private static final ClassInformation JAVA_CLASS = new ClassInformation();

	/** Hashtable holding Informations about known classes */
	private final HashMap<String, ClassInformation> knownClasses = new HashMap<String, ClassInformation>();

	/** Stream to wrap a class byte array to analyze it with {@link ClassAnalyzer#analyze(InputStream)} (avoids double read). */
	private final ByteArrayStream classByteStream = new ByteArrayStream();

	/** Object representing the bootstrap classpath. */
	private final URLClassPath BOOTSTRAP_CP = sun.misc.Launcher.getBootstrapClassPath();

	/** The analyzer for class data. */
	private final ClassAnalyzer analyzer;

	/** The class file adatper. */
	private final TrackedBeanAdapter adapter = new TrackedBeanAdapter(this);

	/**
	 * Default constructor.<br>
	 * Sets this classloader as thread context classloader
	 * 
	 * @param markerInterface The marker interface for the classes to adapt
	 */
	public TrackedBeanInjection(Class markerInterface) {
		Thread.currentThread().setContextClassLoader(this);
		analyzer = new ClassAnalyzer(markerInterface);
		// LATER own bootstrap checker?
		// String[] libs = System.getProperty("sun.boot.class.path").split(File.pathSeparator);
	}

	/**
	 * Launches an the application by calling it's main method.<br>
	 * Use this method to "activate" the Bean Tracking in an application.
	 * 
	 * @param mainClass The name of the class holding the main method
	 * @param args The arguments to pass to the main
	 * @throws Exception If something went wrong
	 */
	public static void launchApplication(String mainClass, String[] args) throws Exception {
		launchApplication(TrackedBean.class, mainClass, args);
	}

	/**
	 * Launches an the application by calling it's main method.<br>
	 * Use this method to "activate" the Bean Tracking in an application.
	 * 
	 * @param markerInterface The marker interface for the classes to adapt
	 * @param mainClass The name of the class holding the main method
	 * @param args The arguments to pass to the main
	 * @throws Exception If something went wrong
	 */
	public static void launchApplication(Class markerInterface, String mainClass, String[] args) throws Exception {
		new TrackedBeanInjection(markerInterface).loadClass(mainClass).getMethod("main", new Class[] { String[].class }).invoke(null, new Object[] { args });
	}

	/**
	 * @see java.lang.ClassLoader#loadClass(java.lang.String)
	 */
	@Override
	public synchronized Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
		// First, check if the class has already been loaded
		Class c = findLoadedClass(name);
		if (c == null) {
			// load java classes via original class loader
			if (name.startsWith("java."))
				return super.loadClass(name, resolve);
			try {
				// gets an input stream to read the bytecode of the class
				final String resource = name.replace('.', '/').concat(".class");
				// delegate to the parent if it is a bootstrap class
				if (BOOTSTRAP_CP.getResource(resource) != null)
					return super.loadClass(name, resolve);
				definePackage(name);
				final InputStream is = getResourceAsStream(resource);
				byte[] b = readClass(is);
				is.close();
				// adapt the class if neccessary
				b = adapt(b);
				c = defineClass(name, b, 0, b.length);
			} catch (final Exception e) {
				// e.printStackTrace();
				throw new ClassNotFoundException(name, e);
			}
		}
		if (resolve)
			resolveClass(c);
		return c;
	}

	/**
	 * Adaptes the passed class byte code if necesary.
	 * 
	 * @param b A byte array holding the class file
	 * @return The adapted bytecode
	 * @throws IOException If something went wrong
	 */
	private byte[] adapt(byte[] b) throws IOException {
		final ClassInformation ci = analyzer.analyze(classByteStream.setContent(b));
		if (ci.tracked || ci.parentTracked)
			return adapter.adapt(b);
		return b;
	}

	/**
	 * Defines the package for the given class name.<br>
	 * If the package allready exists this method simply returns, otherwise the package gets created.
	 * 
	 * @param name The name of the class
	 */
	private void definePackage(final String name) {
		int p = name.lastIndexOf('.');
		if (p < 0)
			return;
		final String pack = name.substring(0, p);
		if (getPackage(pack) != null)
			return;
		definePackage(pack, null, null, null, null, null, null, null);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public ClassInformation getInformation(String name) {
		if (name.startsWith("java/"))
			return JAVA_CLASS;

		// already known?
		ClassInformation known = knownClasses.get(name);
		if (known != null)
			return known;
		String parent = name;
		// all classes of the class structure
		final ArrayList<ClassInformation> structure = new ArrayList<ClassInformation>(2);
		// travers the class structure
		while (parent != null) {
			ClassInformation ci = knownClasses.get(parent);
			if (ci == null)
				try {
					ci = analyzer.analyze(getResourceAsStream(parent.concat(".class")));
				} catch (final Exception e) {
					ci = new ClassInformation();
					ci.internalName = parent;
					System.err.println("Error during class analyzing of " + parent + " " + e.getMessage());
				}
			structure.add(ci);
			parent = ci.parentName;
		}
		// the root of the class structure
		final ClassInformation root = structure.get(structure.size() - 1);
		knownClasses.put(root.internalName, root);
		// set the parentTracked and serializable flags
		boolean parentTracked = root.tracked;
		boolean parentSerializable = root.serializable;
		for (int i = structure.size() - 2; i > -1; i--) {
			final ClassInformation ci = structure.get(i);
			ci.parentTracked = parentTracked;
			ci.serializable |= parentSerializable;
			parentTracked |= ci.tracked;
			parentSerializable |= ci.serializable;
			knownClasses.put(ci.internalName, ci);
		}
		// the class you've asked is the first entry in the list
		return structure.get(0);
	}

	/**
	 * Reads the bytecode of a class.
	 * 
	 * @param is an input stream from which to read the class.
	 * @return the bytecode read from the given input stream.
	 * @throws IOException if a problem occurs during reading.
	 */
	private static byte[] readClass(final InputStream is) throws IOException {
		if (is == null) {
			throw new IOException("Class not found");
		}
		byte[] b = new byte[is.available()];
		int len = 0;
		while (true) {
			final int n = is.read(b, len, b.length - len);
			if (n == -1) {
				if (len < b.length) {
					final byte[] c = new byte[len];
					System.arraycopy(b, 0, c, 0, len);
					b = c;
				}
				return b;
			}
			len += n;
			if (len == b.length) {
				final byte[] c = new byte[b.length + 1000];
				System.arraycopy(b, 0, c, 0, len);
				b = c;
			}
		}
	}

	/**
	 * {@link ByteArrayInputStream} with exchangable byte array.
	 * 
	 * @author M. Hautle
	 */
	private static class ByteArrayStream extends ByteArrayInputStream {
		/** Empty byte array. */
		private static final byte[] EMPTY = new byte[0];

		/**
		 * Default constructor.
		 */
		public ByteArrayStream() {
			super(EMPTY);
		}

		/**
		 * Sets the given byte array a stream content.
		 * 
		 * @param buf The content to set
		 * @return The stream itslef
		 */
		ByteArrayStream setContent(byte[] buf) {
			this.buf = buf;
			this.pos = 0;
			this.count = buf.length;
			return this;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void close() throws IOException {
			setContent(EMPTY);
		}
	}
}
